﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Red.Controls.Utils;

namespace Red.Controls.Extensions
{
    /// <summary>
    ///     Arguments for DragDropExtension.DragStart event
    /// </summary>
    public class DragStartEventArgs : RoutedEventArgs
    {
        /// <summary>
        ///     Data to be dragged - null if drag operation shouldn't start
        /// </summary>
        public Task<IDataObject> DraggedData
        {
            get;
            set;
        }

        public DragStartEventArgs( RoutedEvent routedEvent, object source )
            : base( routedEvent, source )
        {
        }
    }

    public class DragStopEventArgs : RoutedEventArgs
    {
        /// <summary>
        ///     Was the associated action successful 
        /// </summary>
        public bool WasSuccessful { get; set; }

        public DragStopEventArgs( RoutedEvent routedEvent, object source, DragDropEffects dropResult )
            : base( routedEvent, source )
        {
            WasSuccessful = dropResult != DragDropEffects.None;
        }
    }

    /// <summary>
    ///     Sends the information about every drag-and-drop operation happening in the system.
    /// </summary>
    public static class DragBroadcaster
    {
        #region Events

        /// <summary>
        ///     Delegate receiving drag-and-drop broadcasted events
        /// </summary>
        public delegate void DragBroadcastHandler( object sender, IDataObject draggedData );

        /// <summary>
        ///     Raised when a drag-and-drop operation starts
        /// </summary>
        public static event DragBroadcastHandler OnDragStarted;

        /// <summary>
        ///     Raised when a drag-and-drop operation ends
        /// </summary>
        public static event DragBroadcastHandler OnDragEnded;

        #endregion Events

        #region Internal methods

        internal static void BroadcastDragStarted( object sender, IDataObject draggedData )
        {
            if ( OnDragStarted != null )
            {
                OnDragStarted( sender, draggedData );
            }
        }

        internal static void BroadcastDragEnded( object sender, IDataObject draggedData )
        {
            if ( OnDragEnded != null )
            {
                OnDragEnded( sender, draggedData );
            }
        }

        #endregion Internal methods
    }

    /// <summary>
    ///     FrameworkElement's extensions supporting drag-and-drop operations
    /// </summary>
    public static class DragDropExtension
    {
        #region Data

        // This dictionary previously used FrameworkElement handles as the key. Whilst this is fine functionally,
        // entries aren't reliably removed from the dictionary in HandleAllowDragChanged, so these handles prevent
        // the GC from cleaning up, so we're effectively left with a memory leak
        private static readonly Dictionary<int, Point?> _dragStartPoints = new Dictionary<int, Point?>();

        #endregion Data

        #region AllowDrag

        public static readonly DependencyProperty AllowDragProperty =
             DependencyProperty.RegisterAttached( "AllowDrag", typeof( bool ), typeof( FrameworkElement ),
                 new PropertyMetadata( false, HandleAllowDragChanged ) );

        public static bool GetAllowDrag( DependencyObject element )
        {
            return (bool)element.GetValue( AllowDragProperty );
        }

        public static void SetAllowDrag( DependencyObject element, bool value )
        {
            element.SetValue( AllowDragProperty, value );
        }

        private static void HandleAllowDragChanged( DependencyObject obj, DependencyPropertyChangedEventArgs e )
        {
            var element = obj as FrameworkElement;
            if ( element != null )
            {
                element.PreviewMouseLeftButtonDown -= OnPreviewMouseLeftButtonDown;
                element.MouseMove -= OnMouseMove;
                _dragStartPoints.Remove( element.GetHashCode() );
                if ( true.Equals( e.NewValue ) )
                {
                    element.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
                    element.MouseMove += OnMouseMove;
                    _dragStartPoints.Add( element.GetHashCode(), null );
                }
            }
        }

        #endregion AllowDrag

        #region DragStart

        public delegate void DragStartEventHandler( object sender, DragStartEventArgs e );

        public static readonly RoutedEvent DragStartEvent =
            EventManager.RegisterRoutedEvent( "DragStart",
                RoutingStrategy.Bubble, typeof( DragStartEventHandler ), typeof( FrameworkElement ) );

        public static void AddDragStartHandler( DependencyObject obj, DragStartEventHandler handler )
        {
            var element = obj as FrameworkElement;
            if ( element != null )
            {
                element.AddHandler( DragStartEvent, handler );
            }
        }

        public static void RemoveDragStartHandler( DependencyObject obj, DragStartEventHandler handler )
        {
            var element = obj as FrameworkElement;
            if ( element != null )
            {
                element.RemoveHandler( DragStartEvent, handler );
            }
        }

        #endregion DragStart

        #region DragStop

        public delegate void DragStopEventHandler( object sender, DragStopEventArgs e );

        public static readonly RoutedEvent DragStopEvent =
            EventManager.RegisterRoutedEvent( "DragStop",
                RoutingStrategy.Bubble, typeof( DragStopEventHandler ), typeof( FrameworkElement ) );

        public static void AddDragStopHandler( DependencyObject obj, DragStopEventHandler handler )
        {
            var element = obj as FrameworkElement;
            if ( element != null )
            {
                element.AddHandler( DragStopEvent, handler );
            }
        }

        public static void RemoveDragStopHandler( DependencyObject obj, DragStopEventHandler handler )
        {
            var element = obj as FrameworkElement;
            if ( element != null )
            {
                element.RemoveHandler( DragStopEvent, handler );
            }
        }

        #endregion DragStop

        #region ScrollOnDragDrop

        public static readonly DependencyProperty ScrollOnDragProperty =
            DependencyProperty.RegisterAttached( "ScrollOnDrag", typeof( bool ), typeof( FrameworkElement ),
                new PropertyMetadata( false, HandleScrollOnDragChanged ) );

        public static bool GetScrollOnDrag( DependencyObject element )
        {
            return (bool)element.GetValue( ScrollOnDragProperty );
        }

        public static void SetScrollOnDrag( DependencyObject element, bool value )
        {
            element.SetValue( ScrollOnDragProperty, value );
        }

        private static void HandleScrollOnDragChanged( DependencyObject obj, DependencyPropertyChangedEventArgs e )
        {
            var element = obj as FrameworkElement;
            if ( element == null )
            {
                throw new ArgumentException( "Invalid object type" );
            }

            element.PreviewDragOver -= OnContainerPreviewDragOver;
            if ( true.Equals( e.NewValue ) )
            {
                element.PreviewDragOver += OnContainerPreviewDragOver;
            }
        }

        #endregion ScrollOnDragDrop

        #region Event handlers

        private static void OnContainerPreviewDragOver( object sender, DragEventArgs e )
        {
            var container = sender as FrameworkElement;

            if ( container == null )
            {
                return;
            }

            ScrollViewer scrollViewer = Helpers.FindFirstVisualChild<ScrollViewer>( container );

            if ( scrollViewer == null )
            {
                return;
            }

            double tolerance = 60;
            double verticalPos = e.GetPosition( container ).Y;
            double offset = 20;

            if ( verticalPos < tolerance ) // Top of visible list? 
            {
                scrollViewer.ScrollToVerticalOffset( scrollViewer.VerticalOffset - offset ); //Scroll up. 
            }
            else if ( verticalPos > container.ActualHeight - tolerance ) //Bottom of visible list? 
            {
                scrollViewer.ScrollToVerticalOffset( scrollViewer.VerticalOffset + offset ); //Scroll down.     
            }
        }

        private static void OnPreviewMouseLeftButtonDown( object sender, MouseButtonEventArgs e )
        {
            var element = sender as FrameworkElement;

            _dragStartPoints[ element.GetHashCode() ] = e.GetPosition( null );
        }

        private static async void OnMouseMove( object sender, MouseEventArgs e )
        {
            var element = sender as FrameworkElement;
            int elementHash = element.GetHashCode();

            if ( e.LeftButton != MouseButtonState.Pressed )
            {
                _dragStartPoints[ elementHash ] = null;
                return;
            }

            if ( _dragStartPoints[ elementHash ] != null )
            {
                Point curPos = e.GetPosition( null );
                Vector diff = _dragStartPoints[ elementHash ].Value - curPos;

                if ( Math.Abs( diff.X ) > SystemParameters.MinimumHorizontalDragDistance ||
                     Math.Abs( diff.Y ) > SystemParameters.MinimumVerticalDragDistance )
                {
                    _dragStartPoints[ elementHash ] = null;
                    var startArgs = new DragStartEventArgs( DragStartEvent, e.OriginalSource );
                    element.RaiseEvent( startArgs );

                    if ( startArgs.DraggedData == null )
                        return;

                    DragDropEffects result = DragDropEffects.None;
                    IDataObject draggedData = await startArgs.DraggedData;
                    if ( draggedData != null )
                    {
                        DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Scroll;
                        DragBroadcaster.BroadcastDragStarted( element, draggedData );
                        result = System.Windows.DragDrop.DoDragDrop( element, draggedData, allowedEffects ); // this call is blocks for the whole drag&drop operation
                        DragBroadcaster.BroadcastDragEnded( element, draggedData );
                    }

                    var stopArgs = new DragStopEventArgs( DragStopEvent, e.OriginalSource, result );
                    element.RaiseEvent( stopArgs );
                }
            }
        }

        #endregion Event handlers
    }

}